Repository navigation
Conversation
538aa64 to
7d01999
Compare
Retriggers the PR build after an OpenSearch testcontainer infrastructure failure unrelated to the change set. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
a38ff75 to
1449386
Compare
|
Hi @chibenwa |
Arsnael
left a comment
There was a problem hiding this comment.
You add read-range in the BlobStoreDAO interface and I see tests for memory and s3, how is this impacting for other blobstore implementations?
readRange is a default method, so existing implementations inherit it without needing changes. It reads the complete blob through their existing readBytes method, slices the requested range, and preserves the metadata. |
shouldnt this be tested at least on other implems just to make sure that it's not creating issues then? |
Yes, you’re right, I should have covered the other implementations too. Added the contract to File, Cassandra, PostgreSQL, AES, and Zstd tests. |
|
|
||
| private static void validateRange(long start, long end) { | ||
| if (start < 0) { | ||
| Preconditions.checkArgument(start != Long.MIN_VALUE, "'start' must be greater than %s", Long.MIN_VALUE); |
There was a problem hiding this comment.
What does this Long.MIN_VALUE magic value represent ?
I'd keep start >= 0.
There was a problem hiding this comment.
I used negative start values for suffix reads, with this guard rejecting an unrepresentable suffix length. I agree that this overload makes the API less clear. I’ll keep start >= 0 and handle suffix reads explicitly when adding chunk-footer support.
| private static void validateRange(long start, long end) { | ||
| if (start < 0) { | ||
| Preconditions.checkArgument(start != Long.MIN_VALUE, "'start' must be greater than %s", Long.MIN_VALUE); | ||
| Preconditions.checkArgument(end == -1, "'end' must be -1 for a suffix range"); |
There was a problem hiding this comment.
Let's favour explicit typing in our API
If there is no end let's represent this by Optional<Long> and rely on Optional.empty()
There was a problem hiding this comment.
Agreed. Replaced the -1 sentinel with Optional; Optional.empty() now means read through the end. Updated all callers and tests.
| return withMetadata(ContentEncoding.NAME, contentEncoding.asValue()); | ||
| } | ||
|
|
||
| public BlobMetadata withTotalObjectSize(long totalObjectSize) { |
There was a problem hiding this comment.
I do not really understand why this is needed.
Don't you mind giving retionals as per why we'd need this total object size ?
There was a problem hiding this comment.
A range response contains only the requested bytes, so its payload length doesn’t tell us the full object size. The compaction code needs that size to locate the footer and validate slot bounds without downloading the whole chunk.
S3 provides it through Content-Range; the default implementation gets it from the full read. This helper attaches that computed size, replacing any stale value already in the metadata.
There was a problem hiding this comment.
Ok thanks for the answer.
Can you confirm me that this metadata is only set through range reads and never store? (That is what I understand reading the code...
There was a problem hiding this comment.
It’s added only to range-read results; reading doesn’t modify the stored object. However, save() preserves supplied metadata, so explicitly saving that result would persist it. There’s currently no filter preventing that.
chibenwa
left a comment
There was a problem hiding this comment.
Maybe we can implement an optimized version for file storage too, while we are at it. No?
Good work overall BTW. It's way easier to digest in small chuncks.
Good idea, added the optimized file implementation to this PR. It now seeks directly to the requested offset and reads only the required bytes. Tests pass locally, including a range beyond 2 GiB. |
| "'end' (%s) must be greater than or equal to 'start' (%s)", value, start)); | ||
| } | ||
|
|
||
| private static int rangeStart(long start, long totalSize) { |
There was a problem hiding this comment.
Why using int as a type ? Why not long?
There was a problem hiding this comment.
start and totalSize remain long. This helper returns int because the default implementation slices a Java byte array, whose indexes are int. The bounds check ensures the cast is safe. Optimized implementations such as file storage keep offsets as long, including beyond 2 GiB.
| @Test | ||
| void readRangeShouldSeekBeyondTwoGigabytesWithoutReadingWholeFile() throws Exception { |
There was a problem hiding this comment.
Why isnnt this valuable test part of the contract ?
There was a problem hiding this comment.
This test uses a sparse file larger than 2 GiB to verify that the optimized implementation seeks directly. Moving it into the shared contract would require a backend-specific large-object fixture. Backends using the full-read fallback also have byte-array limits, so they couldn’t pass this case. The common range behavior remains covered by the contract.
chibenwa
left a comment
There was a problem hiding this comment.
Really good status to me
Mostly nitpicks.
validateRange is duplicated three times (API, File, S3). It could live in a shared static helper.
Thanks for the serious work here.
Arsnael
left a comment
There was a problem hiding this comment.
Thank you for the follow up on the comments.
I read it and have nothing else to add. Almost there :)
Thanks. Extracted validation into a shared static helper in BlobStoreDAO, reused by the default, File, and S3 implementations. |
Adds inclusive byte-range reads to
BlobStoreDAO, with optimized implementations for S3 and file storage. S3 sends aGetObjectrequest with aRangeheader. File storage seeks directly usingFileChanneland allocates only the requested range, instead of loading the complete file.startmust be non-negative.Optional<Long> endspecifies the inclusive last byte, orOptional.empty()to read through the end. Invalid ranges fail explicitly. Results preserve blob metadata and expose the complete source size, replacing any staletotal-object-sizemetadata.The default implementation reads and slices
readBytesfor backends without an optimized override. The S3 implementation retains the existing SSE-C request builder and namespace fallback behavior. The file implementation closes its channel, schedules blocking work on boundedElastic, and reports unexpected EOF rather than returning incomplete data.Validation
The shared 18-case range-read contract covers Memory, File, Cassandra, PostgreSQL, S3 (Scality CloudServer), AES, and Zstd over Memory/File/S3. Zstd tests additionally force compression and check decoded ranges.
For the file optimization, ran the full
FileBlobStoreDAOTestsuite and Zstd-over-File range tests. Added a sparse-file regression reading four bytes at an offset beyond 2 GiB, verifying the bytes and total size without loading the whole file.Both test suites passed locally, along with Maven Checkstyle.